
__author__="jonathan"
__date__ ="$16-May-2010 17:39:11$"

from event.event_types import EVENT_UPDATE
from event.event_types import EVENT_FLOOR
import numpy
from math import sqrt
from pygame import time

from event.event import GameEvent
from event.event_types import *

# Model constants
GRAVITY = 9.81                          # m/s^2
JUMP_VEL = numpy.array((0., 0., 5.))    # (x,y,z) m/s
MAX_VEL = 10                            # m/s (in direction of target)
MAX_ACC = 10                            # m/s^2
FLOOR_HEIGHT = 0.0                      # m
DEFAULT_POS = numpy.array((0., 0., 0.)) # (x,y,z) m
DEFAULT_VEL = numpy.array((0., 0., 0.)) # (x,y,z) m/s
DEFAULT_DIR = numpy.array((1., 0., 0.)) # (x,y) m - unit vector


# scale from metres to screen units (arbitrary scaling)
SCALE = 50
GRAVITY *= SCALE
JUMP_VEL *= SCALE
MAX_VEL *= SCALE
MAX_ACC *= SCALE
FLOOR_HEIGHT *= SCALE
DEFAULT_POS *= SCALE
DEFAULT_VEL *= SCALE

class GameObject(object):
    """A object that contains the current position & velocity and the objects
    target position and velocity.
    """

    def __init__(self,
            start_pos=DEFAULT_POS,
            start_vel=DEFAULT_VEL,
            direction=DEFAULT_DIR,
            target=None):

        assert len(start_pos) == 3, "start_pos must be a 3 element vector."
        assert len(start_vel) == 3, "start_vel must be a 3 element vector."
        assert len(direction) == 3, "target_vel must be a 3 element vector."
        assert target is None or len(target) == 3, "target must be a 3 element vector."

        # copy the arrays to avoid sharing objects.
        self.position = numpy.array(start_pos, float)
        self.velocity = numpy.array(start_vel, float)
        self.direction = numpy.array(direction, float)
        self.target = numpy.array(target, float) if target else None

        self._is_jumping = False

    @property
    def at_target(self):
        if self.target is None:
            return True
        return numpy.allclose(self.target[:2], self.position[:2], atol=10e-8)

    def jump(self):
        self.velocity += JUMP_VEL
        self._is_jumping = True

    def update(self, ev_manager, time_delta):

        # if we are above the floor then we need to adjust for gravity
        if self.position[2] > FLOOR_HEIGHT:
            self.velocity[2] -= (GRAVITY * time_delta)
        elif self.velocity[2] < 0:
            self.position[2] = 0.
            self.velocity[2] = 0.
            self._is_jumping = False
            if ev_manager:
                ev_manager.post( GameEvent(EVENT_FLOOR) )

        # if we are not jumping then we need to deal with x,y movement now
        if not self._is_jumping:

            # test to avoid overshooting the target
            if self.target is not None:
                if all(abs(self.target[:2] - self.position[:2])
                        < abs(time_delta * self.velocity[:2])):
                    self.position[:2] = self.target[:2]

                # if we are not at the target then move towards it
                if not self.at_target:

                    # get the unit vector towards the target
                    dir = self.target[:2] - self.position[:2]
                    dir *= 1/sqrt(numpy.dot(dir, dir))

                    # now calculate the acceleration
                    vel = self.velocity[:2]
                    vel += (time_delta * MAX_ACC * dir)
                    vel = numpy.sign(vel) * numpy.minimum(abs(vel), abs(MAX_VEL * dir))

                    self.velocity[:2] = vel
                    self.direction = numpy.append(dir, (0,))

                else:
                    self.velocity[:2] = (0., 0.)
                
        # update the players position
        self.position += (self.velocity * time_delta)
        self.position[2] = max(self.position[2], FLOOR_HEIGHT)
        
        if ev_manager:
            ev_manager.post( GameEvent(EVENT_UPDATE, model=self) )



class GameEngine(object):
    """A simple 3D game engine with basic physics.

    Implemented features:
    Acceleration: Constant acceleration in direction of target at a value of
                  MAX_ACC.
    Velocity:     Velocity capped at MAX_VEL.
    Jumping:      Based on instant impulse giving a velocity of JUMP_VEL in one
                  time step.
                  Linear acceleration and deceleration halted during jumping
                  (z > 0), resumed on impact with floor (z = 0).
    Gravity:      Constant gravitational acceleration in negative z axis
                  direction.
    """

    def __init__(self, objects):
        """Initialise the model with the given objects at given starting
        positions and velocity, or DEFAULT_POS if no positions is given and
        DEFAULT_VEL if no velocity is given.

        Params
            objects: List of (start_pos, start_vel) where start_pos and
            start_vel are lists of length 4.
        """
        self.objects = {}
        self.last_time  = time.get_ticks()

        # Add the objects to a dictionary along with an object id which can be
        # used to access them.
        for id, obj in objects.iteritems():
            self.objects[id] = GameObject(**obj)

    def add_object(self, id, obj):
        """Adds a GameObject to the collection of objects to update.
        """
        self.objects[id] = GameObject(**obj)

    def receive(self, event, ev_manager):
        """Recieve and process GameEvents.
        """
        if event.type == EVENT_JUMP:
            self.jump(event.object_id)
        if event.type == EVENT_UPDATE:
            self.update(ev_manager)
        if event.type == EVENT_MOUSE:
            self._target = numpy.array(event.mouse_pos + (0,))

    def jump(self, id):
        """Make the referenced game object jump.
        """
        self.objects[id].jump()

    def get_pos(self, id):
        """Get the position of the GameObject with the given id.
        """
        return self.objects[id].position

    def get_dir(self, id):
        """Get the direction of the GameObject with the given id.
        """
        return self.objects[id].direction

    def get_target(self, id):
        """Get the target of the GameObject with the given id.
        """
        return self.objects[id].target

    def set_target(self, id, target):
        """Set the target of the GameObject with the given id.
        """
        assert len(target) == 3, "target vector must have length 3."
        self.objects[id].target = target

    def at_target(self, id):
        """Returns whether or not the GameObject with the given id is at its
        designated target or not.
        """
        return self.objects[id].at_target

    def update(self, ev_manager, id, time_delta):
        """Update the referenced game object.
        """
        ticks = time.get_ticks()
        if time_delta is None:
            time_delta = (ticks - self.last_time) / 1000.
        
        self.objects[id].update(ev_manager, time_delta)

        self.last_time = ticks

    def update_all(self, ev_manager, time_delta):
        """Update all game objects.
        """
        ticks = time.get_ticks()
        if time_delta is None:
            time_delta = (ticks - self.last_time) / 1000.

        for obj in self.objects.values():
            obj.update(ev_manager, time_delta)

        self.last_time = ticks